menus: Implement scrolling through event capture for touch devices
authorCarlos Garnacho <carlosg@gnome.org>
Mon, 12 Dec 2011 17:11:57 +0000 (18:11 +0100)
committerMatthias Clasen <mclasen@redhat.com>
Thu, 1 Mar 2012 21:25:23 +0000 (16:25 -0500)
This makes overflown menus scrollable via direct manipulation.
Once past the threshold, the item below the pointer is unselected
and scrolling starts.

gtk/gtkmenu.c
gtk/gtkmenuprivate.h

index faa68961602fc729081cb9ea82608975346b7468..f1571d972b6bb68b413814646d953154ae0d43d1 100644 (file)
 #include "gtksettings.h"
 #include "gtkprivate.h"
 #include "gtkwidgetprivate.h"
+#include "gtkdnd.h"
 #include "gtkintl.h"
 #include "gtktypebuiltins.h"
+#include "gtkwidgetprivate.h"
 
 #include "deprecated/gtktearoffmenuitem.h"
 
@@ -225,6 +227,9 @@ static void     gtk_menu_scroll_to         (GtkMenu          *menu,
                                             gint              offset);
 static void     gtk_menu_grab_notify       (GtkWidget        *widget,
                                             gboolean          was_grabbed);
+static gboolean gtk_menu_captured_event    (GtkWidget        *widget,
+                                            GdkEvent         *event);
+
 
 static void     gtk_menu_stop_scrolling         (GtkMenu  *menu);
 static void     gtk_menu_remove_scroll_timeout  (GtkMenu  *menu);
@@ -1064,9 +1069,12 @@ gtk_menu_init (GtkMenu *menu)
   priv->needs_destruction_ref = TRUE;
 
   priv->monitor_num = -1;
+  priv->drag_start_y = -1;
 
   context = gtk_widget_get_style_context (GTK_WIDGET (menu));
   gtk_style_context_add_class (context, GTK_STYLE_CLASS_MENU);
+
+  _gtk_widget_set_captured_event_handler (GTK_WIDGET (menu), gtk_menu_captured_event);
 }
 
 static void
@@ -3323,34 +3331,6 @@ gtk_menu_get_preferred_height_for_width (GtkWidget *widget,
   g_free (nat_heights);
 }
 
-
-
-static gboolean
-gtk_menu_button_scroll (GtkMenu        *menu,
-                        GdkEventButton *event)
-{
-  GtkMenuPrivate *priv = menu->priv;
-
-  if (priv->upper_arrow_prelight || priv->lower_arrow_prelight)
-    {
-      gboolean touchscreen_mode;
-
-      g_object_get (gtk_widget_get_settings (GTK_WIDGET (menu)),
-                    "gtk-touchscreen-mode", &touchscreen_mode,
-                    NULL);
-
-      if (touchscreen_mode)
-        gtk_menu_handle_scrolling (menu,
-                                   event->x_root, event->y_root,
-                                   event->type == GDK_BUTTON_PRESS,
-                                   FALSE);
-
-      return TRUE;
-    }
-
-  return FALSE;
-}
-
 static gboolean
 pointer_in_menu_window (GtkWidget *widget,
                         gdouble    x_root,
@@ -3390,11 +3370,6 @@ gtk_menu_button_press (GtkWidget      *widget,
   if (event->type != GDK_BUTTON_PRESS)
     return FALSE;
 
-  /* Don't pass down to menu shell for presses over scroll arrows
-   */
-  if (gtk_menu_button_scroll (GTK_MENU (widget), event))
-    return TRUE;
-
   /*  Don't pass down to menu shell if a non-menuitem part of the menu
    *  was clicked. The check for the event_widget being a GtkMenuShell
    *  works because we have the pointer grabbed on menu_shell->window
@@ -3424,11 +3399,6 @@ gtk_menu_button_release (GtkWidget      *widget,
   if (event->type != GDK_BUTTON_RELEASE)
     return FALSE;
 
-  /* Don't pass down to menu shell for releases over scroll arrows
-   */
-  if (gtk_menu_button_scroll (GTK_MENU (widget), event))
-    return TRUE;
-
   /*  Don't pass down to menu shell if a non-menuitem part of the menu
    *  was clicked (see comment in button_press()).
    */
@@ -3672,10 +3642,14 @@ gtk_menu_motion_notify (GtkWidget      *widget,
   GtkMenu *menu;
   GtkMenuShell *menu_shell;
   GtkWidget *parent;
+  GdkDevice *source_device;
 
   gboolean need_enter;
 
-  if (GTK_IS_MENU (widget))
+  source_device = gdk_event_get_source_device ((GdkEvent *) event);
+
+  if (GTK_IS_MENU (widget) &&
+      gdk_device_get_source (source_device) != GDK_SOURCE_TOUCHSCREEN)
     {
       GtkMenuPrivate *priv = GTK_MENU(widget)->priv;
 
@@ -4293,10 +4267,11 @@ gtk_menu_enter_notify (GtkWidget        *widget,
       event->mode == GDK_CROSSING_STATE_CHANGED)
     return TRUE;
 
-  source_device = gdk_event_get_source_device (event);
+  source_device = gdk_event_get_source_device ((GdkEvent *) event);
   menu_item = gtk_get_event_widget ((GdkEvent*) event);
 
-  if (GTK_IS_MENU (widget))
+  if (GTK_IS_MENU (widget) &&
+      gdk_device_get_source (source_device) != GDK_SOURCE_TOUCHSCREEN)
     {
       GtkMenuShell *menu_shell = GTK_MENU_SHELL (widget);
 
@@ -4363,6 +4338,7 @@ gtk_menu_leave_notify (GtkWidget        *widget,
   GtkMenu *menu;
   GtkMenuItem *menu_item;
   GtkWidget *event_widget;
+  GdkDevice *source_device;
 
   if (event->mode == GDK_CROSSING_GTK_GRAB ||
       event->mode == GDK_CROSSING_GTK_UNGRAB ||
@@ -4375,7 +4351,10 @@ gtk_menu_leave_notify (GtkWidget        *widget,
   if (gtk_menu_navigating_submenu (menu, event->x_root, event->y_root))
     return TRUE;
 
-  gtk_menu_handle_scrolling (menu, event->x_root, event->y_root, FALSE, TRUE);
+  source_device = gdk_event_get_source_device ((GdkEvent *) event);
+
+  if (gdk_device_get_source (source_device) != GDK_SOURCE_TOUCHSCREEN)
+    gtk_menu_handle_scrolling (menu, event->x_root, event->y_root, FALSE, TRUE);
 
   event_widget = gtk_get_event_widget ((GdkEvent*) event);
 
@@ -4410,6 +4389,142 @@ gtk_menu_leave_notify (GtkWidget        *widget,
   return GTK_WIDGET_CLASS (gtk_menu_parent_class)->leave_notify_event (widget, event);
 }
 
+static gboolean
+pointer_on_menu_widget (GtkMenu *menu,
+                        gdouble  x_root,
+                        gdouble  y_root)
+{
+  GtkMenuPrivate *priv = menu->priv;
+  GtkAllocation allocation;
+  gint window_x, window_y;
+
+  gtk_widget_get_allocation (GTK_WIDGET (menu), &allocation);
+  gdk_window_get_position (gtk_widget_get_window (priv->toplevel),
+                           &window_x, &window_y);
+
+  if (x_root >= window_x && x_root < window_x + allocation.width &&
+      y_root >= window_y && y_root < window_y + allocation.height)
+    return TRUE;
+
+  return FALSE;
+}
+
+static gboolean
+gtk_menu_captured_event (GtkWidget *widget,
+                         GdkEvent  *event)
+{
+  GdkDevice *source_device;
+  gboolean retval = FALSE;
+  GtkMenuPrivate *priv;
+  GtkMenu *menu;
+  gdouble x_root, y_root;
+  guint button;
+  GdkModifierType state;
+
+  menu = GTK_MENU (widget);
+  priv = menu->priv;
+
+  if (!priv->upper_arrow_visible && !priv->lower_arrow_visible)
+    return retval;
+
+  source_device = gdk_event_get_source_device (event);
+  gdk_event_get_root_coords (event, &x_root, &y_root);
+
+  switch (event->type)
+    {
+    case GDK_TOUCH_BEGIN:
+    case GDK_BUTTON_PRESS:
+      if ((!gdk_event_get_button (event, &button) || button == 1) &&
+          gdk_device_get_source (source_device) == GDK_SOURCE_TOUCHSCREEN &&
+          pointer_on_menu_widget (menu, x_root, y_root))
+        {
+          priv->drag_start_y = event->button.y_root;
+          priv->initial_drag_offset = priv->scroll_offset;
+          priv->drag_scroll_started = FALSE;
+        }
+      else
+        priv->drag_start_y = -1;
+
+      priv->drag_already_pressed = TRUE;
+      break;
+    case GDK_TOUCH_END:
+    case GDK_BUTTON_RELEASE:
+      if (priv->drag_scroll_started)
+        {
+          priv->drag_scroll_started = FALSE;
+          priv->drag_start_y = -1;
+          priv->drag_already_pressed = FALSE;
+          retval = TRUE;
+        }
+      break;
+    case GDK_TOUCH_UPDATE:
+    case GDK_MOTION_NOTIFY:
+      if ((!gdk_event_get_state (event, &state) || (state & GDK_BUTTON1_MASK) 
+!= 0) &&
+          gdk_device_get_source (source_device) == GDK_SOURCE_TOUCHSCREEN)
+        {
+          if (!priv->drag_already_pressed)
+            {
+              if (pointer_on_menu_widget (menu, x_root, y_root))
+                {
+                  priv->drag_start_y = y_root;
+                  priv->initial_drag_offset = priv->scroll_offset;
+                  priv->drag_scroll_started = FALSE;
+                }
+              else
+                priv->drag_start_y = -1;
+
+              priv->drag_already_pressed = TRUE;
+            }
+
+          if (priv->drag_start_y < 0 && !priv->drag_scroll_started)
+            break;
+
+          if (priv->drag_scroll_started)
+            {
+              gint offset, view_height;
+              GtkBorder arrow_border;
+              gdouble y_diff;
+
+              y_diff = y_root - priv->drag_start_y;
+              offset = priv->initial_drag_offset - y_diff;
+
+              view_height = gdk_window_get_height (gtk_widget_get_window (widget));
+              get_arrows_border (menu, &arrow_border);
+
+              if (priv->upper_arrow_visible)
+                view_height -= arrow_border.top;
+
+              if (priv->lower_arrow_visible)
+                view_height -= arrow_border.bottom;
+
+              offset = CLAMP (offset, 0, priv->requested_height - view_height);
+              gtk_menu_scroll_to (menu, offset);
+
+              retval = TRUE;
+            }
+          else if (gtk_drag_check_threshold (widget,
+                                             0, priv->drag_start_y,
+                                             0, y_root))
+            {
+              priv->drag_scroll_started = TRUE;
+              gtk_menu_shell_deselect (GTK_MENU_SHELL (menu));
+              retval = TRUE;
+            }
+        }
+      break;
+    case GDK_ENTER_NOTIFY:
+    case GDK_LEAVE_NOTIFY:
+      if (priv->drag_scroll_started)
+        retval = TRUE;
+      break;
+    default:
+      break;
+    }
+
+  return retval;
+}
+
 static void
 gtk_menu_stop_navigating_submenu (GtkMenu *menu)
 {
@@ -5670,7 +5785,6 @@ gtk_menu_real_move_scroll (GtkMenu       *menu,
     }
 }
 
-
 /**
  * gtk_menu_set_monitor:
  * @menu: a #GtkMenu
@@ -5747,11 +5861,13 @@ static void
 gtk_menu_grab_notify (GtkWidget *widget,
                       gboolean   was_grabbed)
 {
+  GtkMenu *menu;
   GtkWidget *toplevel;
   GtkWindowGroup *group;
   GtkWidget *grab;
   GdkDevice *pointer;
 
+  menu = GTK_MENU (widget);
   pointer = _gtk_menu_shell_get_grab_device (GTK_MENU_SHELL (widget));
 
   if (!pointer ||
@@ -5768,6 +5884,8 @@ gtk_menu_grab_notify (GtkWidget *widget,
 
   if (GTK_MENU_SHELL (widget)->priv->active && !GTK_IS_MENU_SHELL (grab))
     gtk_menu_shell_cancel (GTK_MENU_SHELL (widget));
+
+  menu->priv->drag_scroll_started = FALSE;
 }
 
 /**
index 37617c728602e5c15269a478d24a3d45e43a3123..f4583582bef2225c23c5214025065970f3d0a63e 100644 (file)
@@ -99,6 +99,8 @@ struct _GtkMenuPrivate
   guint seen_item_enter       : 1;
   guint ignore_button_release : 1;
   guint no_toggle_size        : 1;
+  guint drag_already_pressed  : 1;
+  guint drag_scroll_started   : 1;
 
   /* info used for the table */
   guint *heights;
@@ -125,6 +127,9 @@ struct _GtkMenuPrivate
   gint navigation_height;
 
   guint navigation_timeout;
+
+  gdouble drag_start_y;
+  gint initial_drag_offset;
 };
 
 G_END_DECLS